# -*- coding: utf-8 -*-
# בש״ד
#
# Miramar CMS for Django - Part of the Cuba Libre Project
# Copyright (C) 2009, Joshua "jag" Ginsberg <jag@flowtheory.net>
#
# Por mi amor, que inspira a mi cada respiración y que echa de Cuba
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import datetime
import tempfile
import urlparse
import magic
from django.db import models
from django.db.models import Q, F
from django.contrib.auth import models as auth
from django.contrib.sites import models as sites
from django.contrib.sites.managers import CurrentSiteManager
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.core import exceptions
from django.http import HttpResponseRedirect
from django.core.files import File
from django.utils.safestring import mark_safe
from django.conf import settings as site_settings
try:
    from PIL import Image
except ImportError:
    raise ImportError, (u'Please install the Python Imaging Library from your '
                        u'package manager or from source from '
                        u'http://www.pythonware.com/products/pil/ and put it '
                        u'in your PYTHONPATH')
try:
    import diff_match_patch
except ImportError:
    raise ImportError, (u'Please obtain diff_match_patch from '
                        u'http://code.google.com/p/google-diff-match-patch/ '
                        u'and install the library in your PYTHONPATH')
try:
    from taino.models import ComponentField
except ImportError:
    raise ImportError, (u'Please obtain Taíno from '
                        u'http://code.google.com/p/django-components/ '
                        u'and install the application in your settings.py')
from miramar import api
from miramar import signals
from miramar import settings
from django.contrib.redirects import models as redirects

mime_magician = magic.open(magic.MAGIC_MIME)
mime_magician.load()
english_magician = magic.open(magic.MAGIC_NONE)
english_magician.load()

class PageManager(CurrentSiteManager):
    def ready_to_publish(self):
        # Pages with a version approved for publishing either without a
        # published version or newer than the published version
        return self.filter(
            Q(published_version__isnull=True,
              pageversion__approved=True) | \
                Q(published_version__isnull=False,
                  pageversion__approved=True,
                  pageversion__time_stamp__gt=F('published_version__time_stamp')
                  )).distinct()
    
    def recently_updated(self):
        # Pages with a version newer than the published version not yet
        # approved for publishing
        return self.filter(
            Q(pageversion__approved=False,
              published_version__isnull=True) | \
                Q(pageversion__approved=False,
                  pageversion__time_stamp__gt=F('published_version__time_stamp')
                  )).distinct()

    def most_stale(self):
        return self.exclude(published_version__isnull=True).order_by(
            '-published_version__time_stamp')
    
    def recent_work(self, user):
        return self.filter(
            Q(published_version__isnull=True,
              pageversion__author=user) | \
                Q(published_version__isnull=False,
                  pageversion__author=user,
                  pageversion__time_stamp__gt=F('published_version__time_stamp')
                  )).distinct()

class Page(models.Model):
    slug = models.SlugField(_(u'URL stub'), db_index=True)
    title = models.CharField(_(u'Title'), max_length=120)
    site = models.ForeignKey(sites.Site)
    published_version = models.OneToOneField('PageVersion', null=True,
                                             related_name='published',
                                             editable=False)
    created_by = models.ForeignKey(auth.User, editable=False)
    markup_engine = ComponentField(interface=api.IMiramarMarkupEngine)

    on_site = PageManager()
    
    def get_absolute_url(self, slug=None):
        if not slug:
            slug = self.slug
        return reverse('miramar_page_detail', args=[slug])
    
    def rename(self, user, new_slug, create_redirect):
        # In the case that a page "foo" gets renamed to "bar", a new page "foo"
        # is created, and the new page "foo" is renamed to "baz", there will
        # be a redirect on file for "foo"->"bar" already. It must be deleted.
        redirects.Redirect.objects.filter(
            site=self.site,
            old_path=self.get_absolute_url()).delete()
        if create_redirect:
            redirect_obj = redirects.Redirect.objects.create(
                site=self.site,
                old_path=self.get_absolute_url(),
                new_path=self.get_absolute_url(new_slug)
                )
        old_slug = self.slug
        self.slug = new_slug
        self.save(send_event=False)
        signals.cms_event.send(sender=self,
                               actor=user,
                               event=signals.PAGE_RENAMED,
                               comment=_((u'Renamed from %(old_slug)s to '
                                          u'%(new_slug)s')) \
                                          % {'old_slug': old_slug,
                                             'new_slug': new_slug})

    def save(self, *args, **kwargs):
        send_event = kwargs.pop('send_event', True)
        user = kwargs.pop('user', None)
        is_new = not self.id
        super(Page, self).save(*args, **kwargs)
        if send_event:
            if is_new:
                signals.cms_event.send(sender=self,
                                       actor=self.created_by,
                                       event=signals.PAGE_CREATED
                                       )
            else:
                signals.cms_event.send(sender=self,
                                       actor=user,
                                       event=signals.PAGE_UPDATED)
    
    def __unicode__(self):
        return self.slug
    
    class Meta:
        ordering = ['slug']

class PageVersion(models.Model):
    page = models.ForeignKey(Page)
    version_tag = models.SlugField(_(u'Version tag'))
    revision = models.PositiveIntegerField(_(u'Revision number'), 
                                           editable=False)
    author = models.ForeignKey(auth.User, verbose_name=_(u'Author'))
    body = models.TextField(_(u'Body'))
    comment = models.CharField(_(u'Comment'), max_length=250, blank=True)
    approved = models.BooleanField(_(u'Approved for publishing'))
    time_stamp = models.DateTimeField(editable=False, auto_now_add=True)
    
    def get_absolute_url(self):
        return reverse('miramar_page_detail_with_revision', 
                       args=[self.page.slug,
                             self.version_tag,
                             self.revision])
    
    def __unicode__(self):
        return u'%s (%s:%s)' % (self.page.slug, 
                                self.version_tag,
                                self.revision)
    
    class Meta:
        ordering = ['version_tag', '-revision']
        permissions = [
            ('can_publish_pageversion', u'Can publish page version'),
            ('can_revise_pageversion', u'Can revise page version'),
            ('can_approve_pageversion', u'Can approve page version'),
            ('can_branch_pageversion', u'Can branch page version')]
    
    def publish(self, publisher):
        # a page version can be published if the user has the publish permission
        # and either approvals are required and the page version is approved or
        # approvals are not required
        can_publish = publisher.has_perm('miramar.can_publish_pageversion')
        is_approved = self.approved
        if can_publish and is_approved:
            old_version = self.page.published_version
            self.page.published_version = self
            self.page.save(send_event=False)
            signals.cms_event.send(sender=self, 
                                   event=signals.PAGEVERSION_PUBLISHED,
                                   actor=publisher)
            signals.cms_event.send(sender=old_version or self, 
                                   event=signals.PAGEVERSION_UNPUBLISHED,
                                   actor=publisher)
        else:
            if not can_publish:
                raise exceptions.PermissionDenied, \
                    _(u'You do not have permission to publish page versions.')
            if not is_approved:
                raise exceptions.PermissionDenied, \
                    _(u'This version has not been approved for publishing.')
        
    def revise(self, author, body, comment):
        # A page version's content should never be modified. Instead, a new
        # revision of the page version should be created. This method takes
        # an existing page version revision and saves new content into a new
        # revision
        if not author.has_perm('miramar.can_revise_pageversion'):
            raise exceptions.PermissionDenied, \
                _(u'You do not have permission to edit a page version.')
        # FIXME: race condition -- do something smart here
        max_revision = PageVersion.objects.filter(
            page=self.page,
            version_tag=self.version_tag).order_by('-revision')[0].revision
        new_obj = PageVersion.objects.create(page=self.page,
                                             version_tag=self.version_tag,
                                             revision=max_revision+1,
                                             author=author,
                                             body=body,
                                             comment=comment,
                                             approved=False)
        signals.cms_event.send(sender=self, event=signals.PAGEVERSION_UPDATED,
                               actor=author, before=self, after=new_obj,
                               comment=comment)
        return new_obj
    
    def approve(self, approved_by):
        # With approvals enabled, a page cannot be published before it is
        # approved for publishing. This method approves a page version for
        # publishing
        if not approved_by.has_perm('miramar.can_approve_pageversion'):
            raise exceptions.PermissionDenied, \
                _(u'You do not have permission to approve a page version.')
        self.approved = True
        self.save()
        signals.cms_event.send(sender=self, event=signals.PAGEVERSION_APPROVED,
                               actor=approved_by)
    
    def diff(self, other_revision):
        # Generate a diff between two revisions. See templatetag render_diff
        # for use in templates
        dmp = diff_match_patch.difff_match_patch()
        diff = dmp.diff_main(self.body, other_revision.body)
        dmp.diff_cleanupSemantic(diff)
        return diff
    
    def branch(self, actor, new_version_tag):
        if not actor.has_perm('miramar.can_branch_pageversion'):
            raise exceptions.PermissionDenied, \
                _(u'You do not have permission to branch a page version.')
        if PageVersion.objects.filter(page=self.page, 
                                      version_tag=new_version_tag).count():
            raise ValueError, _(u'Version tag %s already exists') %\
                new_version_tag
        new_branch = PageVersion(page=self.page,
                                 version_tag=new_version_tag,
                                 revision=1,
                                 author=actor,
                                 body=self.body,
                                 comment=_(u'Branched from %s:%s') % \
                                           (self.version_tag, self.revision),
                                 approved=False)
        new_branch.save()
        return new_branch
    
    def save(self, *args, **kwargs):
        # This method should only be called for the creation of a new page
        # version or change of approval status. Any other commit should call
        # revise() instead.
        new_version = not self.id
        super(PageVersion, self).save(*args, **kwargs)
        if self.revision == 1 and new_version:
            signals.cms_event.send(sender=self, 
                                   event=signals.PAGEVERSION_CREATED,
                                   actor=self.author)

class RevisionLog(models.Model):
    page = models.ForeignKey(Page, db_index=True)
    actor = models.ForeignKey(auth.User)
    time_stamp = models.DateTimeField(auto_now_add=True, editable=False)
    event_type = models.PositiveIntegerField(_(u'Event type'),
                                             choices=signals.EVENT_TYPES)
    page_version = models.ForeignKey(PageVersion, null=True)
    other_version = models.ForeignKey(PageVersion, related_name='other_version',
                                      null=True)
    comment = models.TextField(blank=True)
    
    def __unicode__(self):
        return _(u'%s %s %s') % (self.page_version, 
                                 self.get_event_type_display(),
                                 self.time_stamp.strftime('%Y-%m-%d %H:%M:%S'))
    
    class Meta:
        ordering = ['page', '-time_stamp']
        permissions = [
            ('can_view_revision_log', u'Can view revision log for page'),
            ]

def log_events(sender=None, event=None, actor=None, before=None, after=None,
               comment='', **kwargs):
    if isinstance(sender, PageVersion):
        page = sender.page
        page_version = sender or before
    else:
        page = sender
        page_version = None
    RevisionLog.objects.create(page=page,
                               actor=actor,
                               event_type=event[0],
                               page_version=page_version,
                               other_version=after,
                               comment=comment)
signals.cms_event.connect(log_events)
    
class StoredImage(models.Model):
    title = models.CharField(_(u'Title'), max_length=120)
    slug = models.SlugField(_(u'Alias URL stub'))
    height = models.PositiveIntegerField(editable=False)
    width = models.PositiveIntegerField(editable=False)
    file = models.ImageField(_(u'Image File'),
                             max_length=240,
                             upload_to=settings.UPLOAD_IMAGE_TO,
                             height_field='height',
                             width_field='width')
    thumbnail = models.ImageField(editable=False,
                                  max_length=250,
                                  upload_to=settings.UPLOAD_IMAGE_TO)
    small = models.ImageField(editable=False,
                              max_length=250,
                              upload_to=settings.UPLOAD_IMAGE_TO)
                                  
    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        return self.thumbnail.url

    def img_tag(self):
        return mark_safe(
            (u'<img src="%(url)s" width="%(width)d" height="%(height)d" '
             u'alt="%(title)s" />') % {
            'url': self.file.url,
            'width': self.width,
            'height': self.height,
            'title': self.title})
    img_tag.allow_tags = True
    
    def thumbnail_preview(self):
        return mark_safe(
            (u'<a href="%(small_url)s" class="preview"><img src="%(url)s" '
             u'alt="%(title)s" /></a>') % {
            'url': self.thumbnail.url,
            'title': self.title,
            'small_url': self.small.url})
    thumbnail_preview.allow_tags = True

    def redirect(self):
        return HttpResponseRedirect(self.file.url)

    def generate_scaled(self):
        thumb_obj = Image.open(open(self.file.path))
        small_obj = thumb_obj.copy()
        thumb_obj.thumbnail(settings.THUMBNAIL_SIZE, Image.ANTIALIAS)
        small_obj.thumbnail(settings.SMALL_IMAGE_SIZE, Image.ANTIALIAS)

        img_filename = os.path.basename(self.file.name)
        fd, tmp_filename = tempfile.mkstemp(suffix=img_filename)
        ofs = open(tmp_filename, 'w')
        thumb_obj.save(ofs)
        ofs.close()
        ifs = open(tmp_filename, 'r')
        file_obj = File(ifs)
        self.thumbnail.save(settings.THUMBNAIL_PREFIX+img_filename,
                            file_obj,
                            save=False)
        ifs.close()

        ofs = open(tmp_filename, 'w')
        small_obj.save(ofs)
        ofs.close()
        ifs = open(tmp_filename, 'r')
        file_obj = File(ifs)
        self.small.save(settings.SMALL_IMAGE_PREFIX+img_filename,
                        file_obj,
                        save=False)
        self.save(generate_scaled=False)

    def save(self, *args, **kwargs):
        generate_scaled = kwargs.pop('generate_scaled', True)
        super(StoredImage, self).save(*args, **kwargs)
        if generate_scaled:
            self.generate_scaled()
            
    def get_mime_type(self):
        return mime_magician.file(self.file.path)
    
    def get_file_type(self):
        return english_magician.file(self.file.path)
    
    class Meta:
        permissions = [('can_view_imagelibrary', u'Can view image library'),]


class StoredFile(models.Model):
    title = models.CharField(_(u'Title'), max_length=120)
    slug = models.SlugField(_(u'Alias URL stub'))
    file = models.FileField(_(u'Published File'),
                             max_length=240,
                             upload_to=settings.UPLOAD_FILE_TO)
                                  
    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        return self.file.url

    def redirect(self):
        return HttpResponseRedirect(self.file.url)

    def get_mime_type(self):
        return mime_magician.file(self.file.path)
    
    def get_file_type(self):
        return english_magician.file(self.file.path)
    
    class Meta:
        permissions = [('can_view_filelibrary', u'Can view file library'),]
